#!/usr/bin/python
# Raspberry Pi Cell and Current Sensor logger for SmartBMS with OZMicro 
# pySmartBMS-Logger version 1.0alpha
#
# Requirements:
# 	- RPi I2C bus connected with OZMicro
#	- Installed package: python-rrdtool
#	- Installed package: python-smbus (>=0.5.1)
#
# Aleksey Isakov <wizemanold@gmail.com>

import smbus
import time
import sys
import rrdtool
import RPi.GPIO as GPIO
import re
import json

# OZMicro chip i2c address
HWADDR = 0x30
# System I2C bus with chip connecter
BUS = 1
# 'LED Warn' pin number on GPIO
LED_WARN = 23
# Power down button pin
BTN_PWRD = 24
# Path to RRD files
RRD_PATH = '/tmp'
# RRD database file name
RRD_FNAME = 'smartbms2.rrd'
# Path to graphs files
GRAPH_PATH = '/tmp'
# Graph file name
GRAPH_FNAME = 'smartbms2.png'
# XML file with data
XML_FNAME = 'smartbms2.xml'
# Wheel travel length in meters
wheelTR = 2.080

# Background work or interactivelly
DAEMON = 0
# Current speed
speed = 0
# odometer
odo = 0
tlast = time.time()

def ssleep(value):
	try:
		for i in range(0,value*5,1):
			time.sleep(0.2)
	except KeyboardInterrupt:
		GPIO.cleanup()
		print '\x1B[sCTRL-C pressed\x1B[u\x1B[?25h'
		exit()
	except:
		print '\x1B[sUnexpected exception\x1B[u'
		raise
	return

# Read from OZMicro EEPROM memory
# addr - EEPROM Memory address 00-7F
# Return - word from EEPROM address
def read_eeprom(addr):
	global bus
	bus_ready = 0
	# Turn on 'LED Warn'
	GPIO.output(LED_WARN, True)
	while (not bus_ready):
		try:
			# Wait until EEPROM busy flag set
			while (bus.read_byte_data(HWADDR, 0x5f) & 0x80) == 0x80:
				pass
			# Put address into EEPROM address reg
			bus.write_byte_data(HWADDR, 0x5e, addr)
			# Put into EEPROM control register Mode=EEPROM and Access=Word_Read
			bus.write_byte_data(HWADDR, 0x5f, 0x55)
			# Wait until EEPROM busy flag set
			while (bus.read_byte_data(HWADDR, 0x5f) & 0x80) == 0x80:
				pass
			# Read back data from EEPROM address
			retval = bus.read_byte_data(HWADDR, 0x5c)
			retval = retval | (bus.read_byte_data(HWADDR, 0x5d) << 8)
			# Turn off EEPROM mode because in this mode ADC do not work
			bus.write_byte_data(HWADDR, 0x5f, 0x00)
			# Wait until EEPROM busy flag set
			while (bus.read_byte_data(HWADDR, 0x5f) & 0x80) == 0x80:
				pass
		# Handle exceptions
		except IOError as e:
			print_screen([0,0,0,0,0],'\x1B[31;1mnot connected\x1B[m',[0]*12,0)
			print "\x1B[sI/O error({0}): {1}\x1B[u".format(e.errno, e.strerror)
			ssleep(1)
        	except KeyboardInterrupt:
                	GPIO.cleanup()
                	print '\x1B[sCTRL-C pressed\x1B[u\x1B[?25h'
			exit()
		except:
			print "\x1B[sUnexpected error\x1B[u"
			ssleep(1)
		else:
			bus_ready = 1
			# Turn off 'LED Warn'
			GPIO.output(LED_WARN, False)
	# Return WORD from EEPROM
	return retval

# Read from OZMicro operational register address
# addr - OpRegister address 00-7F
# Return - Byte from OpRegister
def read_opreg(addr):
	global bus
	bus_ready = 0
	# Turn on 'LED Warn'
	GPIO.output(LED_WARN, True)
	while (not bus_ready):
		try:
			# Read byte from OpRegister
			retval = bus.read_byte_data(HWADDR, addr)
		# Handle exceptions
		except IOError as e:
			print_screen([0,0,0,0,0],'\x1B[31;1mnot connected\x1B[m',[0]*12,0)
			print "\x1B[sI/O error({0}): {1}\x1B[u".format(e.errno, e.strerror)
			ssleep(1)
        	except KeyboardInterrupt:
                	GPIO.cleanup()
                	print '\x1B[sCTRL-C pressed\x1B[u\x1B[?25h'
			exit()
		except:
			print "\x1B[sUnexpected error\x1B[u"
			ssleep(1)
		else:
			bus_ready = 1
			# Turn off 'LED Warn'
			GPIO.output(LED_WARN, False)
	# Return readed byte
	return retval

# RRD database create
def rrd_create():
	data_sources=[ 'DS:cell1:GAUGE:10:2.8:4.5',
			'DS:cell2:GAUGE:10:2.8:4.5',
			'DS:cell3:GAUGE:10:2.8:4.5',
			'DS:cell4:GAUGE:10:2.8:4.5',
			'DS:cell5:GAUGE:10:2.8:4.5',
			'DS:cell6:GAUGE:10:2.8:4.5',
			'DS:cell7:GAUGE:10:2.8:4.5',
			'DS:cell8:GAUGE:10:2.8:4.5',
			'DS:cell9:GAUGE:10:2.8:4.5',
			'DS:cell10:GAUGE:10:2.8:4.5',
			'DS:cell11:GAUGE:10:2.8:4.5',
			'DS:cell12:GAUGE:10:2.8:4.5',
			'DS:current:GAUGE:10:0:35' ]

	rrdtool.create( RRD_PATH + '/' + RRD_FNAME,
			 '--step', '1',
			 data_sources,
			 'RRA:AVERAGE:0.5:1:3600',
			 'RRA:AVERAGE:0.5:10:4320',
			 'RRA:AVERAGE:0.5:60:1440' )
	return

# RRD database update and create graphs, XMLs
def rrd_update(cellv,current):
	rrdtool.update( RRD_PATH + '/' + RRD_FNAME, 'N:%f:%f:%f:%f:%f:%f:%f:%f:%f:%f:%f:%f:%f' % (cellv[0],cellv[1],cellv[2],cellv[3],cellv[4],cellv[5],cellv[6],cellv[7],cellv[8],cellv[9],cellv[10],cellv[11],current))
	# TODO: Make rrdtool-graph more recently than rrdtool-update for reduce I/Os (or just place graphs at memory disk or memcached)
#	rrdtool.graph(GRAPH_PATH + '/' + GRAPH_FNAME,
#			'--imgformat', 'PNG',
#			'--width', '900',
#			'--height', '540',
#			'--vertical-label', 'cell voltage',
#			'--step', '1',
#			'--start', 'end-2h',
#			'--end', 'now',
#			'--title', 'Cell voltage vs Current',
#			'--right-axis-label', 'Current',
#			'--right-axis=18:-50.4',
#			'--color', 'BACK#333333',
#			'--color', 'CANVAS#333333',
#			'--color', 'SHADEA#CCCCCC',
#			'--color', 'SHADEB#CCCCCC',
#			'--color', 'FONT#FFFFFF',
#			'--color', 'AXIS#FFFFFF',
#			'--color', 'ARROW#FF0000',
#			'--color', 'GRID#CCCCCC',
#			'--color', 'MGRID#CCCCCC', 
#			'DEF:voltage1=' + RRD_PATH + '/' + RRD_FNAME + ':cell1:AVERAGE',
#			'DEF:voltage2=' + RRD_PATH + '/' + RRD_FNAME + ':cell2:AVERAGE',
#			'DEF:voltage3=' + RRD_PATH + '/' + RRD_FNAME + ':cell3:AVERAGE',
#			'DEF:voltage4=' + RRD_PATH + '/' + RRD_FNAME + ':cell4:AVERAGE',
#			'DEF:voltage5=' + RRD_PATH + '/' + RRD_FNAME + ':cell5:AVERAGE',
#			'DEF:voltage6=' + RRD_PATH + '/' + RRD_FNAME + ':cell6:AVERAGE',
#			'DEF:voltage7=' + RRD_PATH + '/' + RRD_FNAME + ':cell7:AVERAGE',
#			'DEF:voltage8=' + RRD_PATH + '/' + RRD_FNAME + ':cell8:AVERAGE',
#			'DEF:voltage9=' + RRD_PATH + '/' + RRD_FNAME + ':cell9:AVERAGE',
#			'DEF:voltage10=' + RRD_PATH + '/' + RRD_FNAME + ':cell10:AVERAGE',
#			'DEF:voltage11=' + RRD_PATH + '/' + RRD_FNAME + ':cell11:AVERAGE',
#			'DEF:voltage12=' + RRD_PATH + '/' + RRD_FNAME + ':cell12:AVERAGE',
#			'DEF:curr=' + RRD_PATH + '/' + RRD_FNAME + ':current:AVERAGE',
#			'CDEF:current=curr,18,/',
#			'CDEF:current1=current,2.8,+',
#			'LINE1:2.8',
#			'AREA:current#005500:Current:STACK',
#			'LINE2:current1#AA00AA',
#			'LINE1:voltage1#000000:Cell1',
#			'LINE1:voltage2#155555:Cell2',
#			'LINE1:voltage3#2AAAAA:Cell3',
#			'LINE1:voltage4#3FFFFF:Cell4',
#			'LINE1:voltage5#555554:Cell5',
#			'LINE1:voltage6#6AAAA9:Cell6',
#			'LINE1:voltage7#7FFFFE:Cell7',
#			'LINE1:voltage8#955553:Cell8',
#			'LINE1:voltage9#AAAAA8:Cell9',
#			'LINE1:voltage10#BFFFFD:Cell10',
#			'LINE1:voltage11#D55552:Cell11',
#			'LINE1:voltage12#EAAAA7:Cell12')
	return

# Power down function
def btn_pwrd_callback():
	print '\x1B[sPower button pressed, initiate shutdown now\x1B[u'
	# TODO: Make copy of RRD database if it on tmpfs storage
	# Note: with 47mF capacitor charged to 48V, we have aproximally 37W (down to converter low voltage cut-off). On 35-37W rpi may work about 5-7 seconds
	os.system("sudo shutdown -h now")

# Speed and distance calc func
def speed_sensor_callback():
	global speed,odo,tlast
	tnow = time.time()
	speed = (3600/(tnow-tlast)) * (wheelTR / 1000)
	odo += (wheelTR / 1000)
	tlast = tnow

# Prints screen with values
def print_screen(info_regs,status,voltages,current):
	global redraw
	min_v = 255
	max_v = 0
	jdata = {}
	if (redraw):
		print '\x1B[?25l\x1B[2J\x1B[HStatus:'
		for i in range(0,len(voltages)):
			print 'Cell-%02d:' % (i+1)
		print 'V delta:\n'
		print 'Battery voltage:'
		print 'Current:'
		print 'Internal temp:'
		print 'External probe temp:'
		print '--------------------------'
		print 'State (20):'
		print 'FET Disable condition(1f):'
		print 'Fault condition(1c):'
		
		redraw = 0
	row = 1
	col = 10
	battvol = 0
	print '\x1B[%d;%df%s' % (row,col,status)
	if (re.match(r'not', status)):
		jdata['status'] = 'not connected'
	else:
		jdata['status'] = 'connected'
	jdata['cells'] = [None]*len(voltages)
	for i in range(0,len(voltages)):
		col = 10
		row = i + 2
		color = 32
		if (voltages[i] < min_v): min_v = voltages[i]
		if (voltages[i] > max_v): max_v = voltages[i]
		if (voltages[i] <= 3.55): color = 31
		elif (voltages[i] > 3.55 and voltages[i] <= 3.65): color = 33
		elif (voltages[i] > 3.65 and voltages[i] <= 4.18): color =  32
		elif (voltages[i] > 4.18 and voltages[i] <= 4.192): color = 33
		else: color = 31
		print '\x1B[%d;%df\x1B[%d;1m%2.4f (V)\x1B[m' % (row,col,color,voltages[i])
		jdata['cells'][i] = {"num" : (i+1), "color": color, "voltages": voltages[i]}
		battvol += voltages[i]
	row = i+3
	delta = (max_v - min_v) * 1000
	col = 10
	color = 32
	if (delta >= 250): color = 31
	elif (delta < 250 and delta >= 100): color = 33
	else: color = 32
	print '\x1B[%d;%df\x1B[%d;1m%3d (mV)\x1B[m' % (row,col,color,delta)
	jdata['vdelta'] = delta;
	row += 2
	print '\x1B[%d;%df\x1B[1m%3.4f (V)\x1B[m' % (row,col+8,battvol)
	jdata['vpack'] = battvol
	row += 1
	print '\x1B[%d;%df\x1B[1m%3.6f (A)\x1B[m' % (row,col,current)
	jdata['current'] = current
	row += 1
	print '\x1B[%d;%df\x1B[1m%2.2f (*C)\x1B[m' % (row,col+6,info_regs[3])
	jdata['itemp'] = info_regs[3]
	row += 1
	print '\x1B[%d;%df\x1B[1m%2.2f (mV)\x1B[m' % (row,col+12,info_regs[4])
	row += 2
	dstate = 'IDLE     '
	if (info_regs[0] & 0x08 == 0x08): dstate = 'CHARGE   '
	elif (info_regs[0] & 0x04 == 0): dstate = 'DISCHARGE'
	print '\x1B[%d;%df\x1B[1m%s\x1B[m' % (row,13,dstate)
	jdata['dstate'] = dstate
	row += 1
	fstate = []
	if (info_regs[1] & 0x20 == 0x20): fstate.append('UT_DSBL')
	if (info_regs[1] & 0x10 == 0x10): fstate.append('OT_DSBL')
	if (info_regs[1] & 0x08 == 0x08): fstate.append('SC_DSBL')
	if (info_regs[1] & 0x04 == 0x04): fstate.append('OC_DSBL')
	if (info_regs[1] & 0x02 == 0x02): fstate.append('UV_DSBL')
	if (info_regs[1] & 0x01 == 0x01): fstate.append('OV_DSBL')
	if (len(fstate) == 0): fstate.append('ALL_CLEAR')
	print '\x1B[%d;%df\x1B[K\x1B[1m%s\x1B[m' % (row,28,'|'.join(fstate))
	jdata['fstate'] = '|'.join(fstate)
	row += 1
	ystate = []
	if (info_regs[2] & 0x80 == 0x80): ystate.append('OT_YES')
	if (info_regs[2] & 0x40 == 0x40): ystate.append('UT_YES')
	if (info_regs[2] & 0x20 == 0x20): ystate.append('OV_YES')
	if (info_regs[2] & 0x10 == 0x10): ystate.append('CUPF_YES')
	if (info_regs[2] & 0x08 == 0x08): ystate.append('MFPF_YES')
	if (info_regs[2] & 0x04 == 0x04): ystate.append('VHPF_YES')
	if (info_regs[2] & 0x02 == 0x02): ystate.append('VLPF_YES')
	if (info_regs[2] & 0x01 == 0x01): ystate.append('UV_YES')
	if (len(ystate) == 0): ystate.append('ALL_CLEAR')
	print '\x1B[%d;%df\x1B[K\x1B[1m%s\x1B[m' % (row,22,'|'.join(ystate))
	jdata['ystate'] = '|'.join(ystate)
	fd = open( RRD_PATH + '/' + XML_FNAME + '.tmp', 'w')
	#fd.write(json.dumps(jdata, sort_keys=True, indent=2))
	#fd.write(re.sub(r'\s+', '', json.dumps(jdata)))
	fd.write(json.dumps(jdata))
	fd.close()
	return

# Main function
def main():
	# Check RRD database file exists and correct, recreate if not
	try:
		print 'trying rrd file: ' + RRD_PATH + '/' + RRD_FNAME,
		data = rrdtool.info(RRD_PATH + '/' + RRD_FNAME)
	except:
		print "- rrd file not found or incorrect, recreating..."
		rrd_create()
	else:
		print "- ok"
	# Disable GPIO warnigs
	GPIO.setwarnings(False)
	# Use GPIO numbers not pin numbers
	GPIO.setmode(GPIO.BCM)
	GPIO.setup(BTN_PWRD, GPIO.IN, pull_up_down=GPIO.PUD_UP)
	GPIO.setup(LED_WARN, GPIO.OUT)
	GPIO.add_event_detect(BTN_PWRD, GPIO.FALLING, callback=btn_pwrd_callback)

	# Get shunt resistance (RSENS) value from EEPROM
	rsens = (read_eeprom(0x34) & 0xff) * 0.0001
	#rsens = 0.0022
	# Get battery type and cell numbers
	val = read_opreg(0x06)
	cellcnt = val & 0x0f
	celltype = (val & 0x30) >> 4
	if celltype != 3: # Program work only with LiPO type batteries
		print "Bad battery type"
		exit()
	vols = [None]*cellcnt
	
	# Main loop, until forever
	try:
		while True:
			# Get cell voltages from ADC
			for i in range(0,cellcnt*2,2):
				vall = read_opreg(0x32 + i) >> 3
				valh = read_opreg(0x33 + i)
				voltage = vall | (valh << 5)
				voltage = (voltage * 1.22) / 1000
				vols[i/2] = voltage
				#print '%2d %2.4f' % (i/2,voltage)
			# Get current sensor voltage from ADC
			vall = read_opreg(0x54)
			valh = read_opreg(0x55)
			val = vall | (valh << 8)
			if (val & 0x8000) == 0x8000:
				val = val - 0x10000;
			val = (val * 7.63) / 1000000
			vv1 = read_opreg(0x20) # Read charge/discharge state
			vv2 = read_opreg(0x1f) # Read FET Disable register
			vv3 = read_opreg(0x1c) # Read YES event state register
			vv4 = read_opreg(0x52) >> 3 | (read_opreg(0x53)  << 5) # Read Internal Temp ADC data
			vv4 = (((vv4 * 0.61) - 432)/2.0976) - 40 # Value with 0.61mV LSB. -40 = 431.51mV. As temp arise voltage arise by 2.0976mV
			vv5 = (read_opreg(0x4c)  >> 3 | (read_opreg(0x4d)  << 5)) * 0.61 # TODO Need find on that GPIO1-3 external temp probe is located, OpREG to read 0x2d (GPIO Mode reg)
											# 	after what read right reg GPIO1(0x4c-0x4d), GPIO2(0x4e-0x4f), GPIO3(0x50-0x51)
			#print 'data: %d %d' % (vv1, vv2)
			# Calc current
			curr = val / rsens
			print_screen([vv1,vv2,vv3,vv4,vv5],'\x1B[32;1mconnected    \x1B[m',vols,curr)
			# Take absolute current value for RRD graphs
			if curr < 0:
				curr = 0 - curr
			#print 'curr: %3.6f' % curr
			# Update RRD database file with new data
			rrd_update(vols,curr)
			# Read data from debug purposes
			# Wait 1 second before next OZMicro query
			ssleep(1)
	except KeyboardInterrupt:
		GPIO.cleanup()
		print '\x1B[sCTRL-C pressed\x1B[u\x1B[?25h'
		exit()
	except:
		print "\x1B[sUnexpected exception\x1B[u"
		raise
	
# Redraw screen
redraw = 1
if __name__ == '__main__':
	bus = smbus.SMBus(BUS)
	main()
	GPIO.cleanup()
	#print_screen([0,0xff,0xff,0,0],'\x1B[31;1mnot connected\x1B[m',[3.4,3.65,3.75,3.84,3.99,4.02,4.18,4.2,4.19,3.75,3.33,3.84],10)
	print '\x1B[m\x1B[?25h'
	exit(0)
